﻿using System;
using System.Globalization;
using Notoric.Commanding.CommandBase;

namespace Notoric.ViewModel.Elements.CWNotation
{
    /// <summary>
    /// Adapts a <see cref="Model.Elements.CWNotation.Note"/> instance to a Windows Presentation Foundation view.
    /// </summary>
    public class Note : ViewModelElement
    {
        #region Constructors

        /// <summary>
        /// Creates a new <see cref="Note"/> instance.
        /// </summary>
        /// <param name="owner">
        /// <para>
        /// Type: <see cref="Chord"/>
        /// </para>
        /// <para>
        /// The element that directly owns this element.
        /// </para>
        /// </param>
        /// <param name="adaptedElement">
        /// <para>
        /// Type: <see cref="Model.Elements.CWNotation.Note"/>
        /// </para>
        /// <para>
        /// The adapted model element instance.
        /// </para>
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="adaptedElement"/> is <see langword="null"/>.
        /// </exception>
        /// <exception cref="NullReferenceException">
        /// <paramref name="owner"/> is <see langword="null"/>.
        /// The values of the <see cref="ViewModelElement.UndoStack"/> and <see cref="ViewModelElement.RedoStack"/> properties
        /// are passed directly to the base class (<see cref="ViewModelElement"/>) constructor. Thus, there is not
        /// an occasion to react with a more appropriate exception.
        /// </exception>
        public Note(Chord owner, Model.Elements.CWNotation.Note adaptedElement)
            : base(adaptedElement, owner, owner.UndoStack, owner.RedoStack)
        {
            Accidental = new Accidental(this, adaptedElement.Accidental);

            RegisterToOwnerPropertyChanged<Chord>();
            RegisterToOwnerPropertyChanged<Staff>();
        }

        #endregion

        #region ViewModelElement Members

        /// <inheritdoc/>
        public override string ElementNameKey
        {
            get { return elementNameKey; }
        }

        /// <inheritdoc/>
        public override void Dispose()
        {
            UnregisterFromOwnerPropertyChanged<Chord>();
            UnregisterFromOwnerPropertyChanged<Staff>();
            base.Dispose();
        }

        #endregion

        #region Event Handlers

        /// <inheritdoc/>
        protected override bool AdaptedElement_PropertyChanged(global::System.ComponentModel.PropertyChangedEventArgs e)
        {
            if (e.PropertyName == "HeadPosition" || e.PropertyName == "Width")
                RaisePropertyChanged("HeadPosition");
            else if (e.PropertyName == "StaffPosition")
                RaisePropertyChanged("VerticalPosition");
            
            return base.AdaptedElement_PropertyChanged(e);
        }

        /// <inheritdoc/>
        protected override void Owner_PropertyChanged(ViewModelElement owner, global::System.ComponentModel.PropertyChangedEventArgs e)
        {
            if (owner is Chord)
            {
                if (e.PropertyName == "Position")
                    RaisePropertyChanged("HeadPosition");
                if (e.PropertyName == "VerticalPosition")
                    RaisePropertyChanged("DotsPosition");
            }
            else if (owner is Staff)
            {
                if (e.PropertyName == "Top" || e.PropertyName == "SpaceSize")
                    RaisePropertyChanged("VerticalPosition");
            }
        }

        #endregion

        #region Public Methods

        /// <summary>
        /// Creates an undoable command for setting the <see cref="StaffPosition"/> of the symbol.
        /// </summary>
        /// <param name="position">
        /// <para>
        /// Type: <see cref="Int32"/>
        /// </para>
        /// <para>
        /// The new staff position.
        /// </para>
        /// </param>
        /// <returns>
        /// <para>
        /// Type: <see cref="UndoableCommandBase"/>
        /// </para>
        /// <para>
        /// The created command ready for execution.
        /// </para>
        /// </returns>
        public UndoableCommandBase CreateSetStaffPositionCommand(int position)
        {
            return new SetterCommand<int>(this, "StaffPosition", position, UndoStack, RedoStack);
        }

        /// <summary>
        /// Creates an undoable command for setting the <see cref="TypeSize"/> property.
        /// </summary>
        /// <param name="typeSize">
        /// <para>
        /// Type: <see cref="Double"/>
        /// </para>
        /// <para>
        /// The new type size.
        /// </para>
        /// </param>
        /// <returns>
        /// <para>
        /// Type: <see cref="UndoableCommandBase"/>
        /// </para>
        /// <para>
        /// The command ready for use.
        /// </para>
        /// </returns>
        public UndoableCommandBase CreateSetTypeSizeCommand(double typeSize)
        {
            var modelSize = (double)LengthConverter.ConvertBack(typeSize, typeof(double), null, CultureInfo.CurrentCulture);

            return new SetterCommand<double>(this, "TypeSize", modelSize, UndoStack, RedoStack);
        }

        #endregion

        #region Public Properties

        /// <summary>
        /// Gets the accidental of this note.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="Accidental"/>
        /// </para>
        /// <para>
        /// The accidental of this note (this value is never <see langword="null"/>).
        /// </para>
        /// </value>
        public Accidental Accidental
        {
            get;
            private set;
        }

        /// <summary>
        /// Gets the vertical position of the augmentation dots.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="Double"/>
        /// </para>
        /// <para>
        /// The vertical offset (in device independent pixels) of the note's augmentation dots
        /// from the section border (includes the owner <see cref="Staff"/>'s position).
        /// </para>
        /// </value>
        public double DotsPosition
        {
            get { return FindOwner<Staff>().Bottom - (FindOwner<Staff>().SpaceSize / 2) * (((Model.Elements.CWNotation.Note)AdaptedElement).DotsPosition + StaffPosition); }
        }

        /// <summary>
        /// Gets the effective type size of the note.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="Double"/>
        /// </para>
        /// <para>
        /// The effective type size (in device independent pixels) of the note.
        /// </para>
        /// </value>
        public double EffectiveTypeSize
        {
            get
            {
                var size = TypeSize;
                if (double.IsNaN(size))
                    return (double)LengthConverter.Convert(((Model.Elements.CWNotation.Note)AdaptedElement).EffectiveTypeSize, typeof(double), null, CultureInfo.CurrentCulture);

                return size;
            }
        }

        /// <summary>
        /// Gets the position of the note's head.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="Double"/>
        /// </para>
        /// <para>
        /// The horizontal offset (in device independent pixels) of the note's head
        /// from the section border (it includes the position of the owner <see cref="Chord"/>).
        /// </para>
        /// </value>
        public double HeadPosition
        {
            get
            {
                var headPosition = GetPropertyValue<Model.Elements.CWNotation.NotePosition>("HeadPosition");

                // 0.76px = 0.2mm is the stroke of the stem line

                if (headPosition == Model.Elements.CWNotation.NotePosition.Left)
                    return FindOwner<Chord>().Position - Width - 0.76;
                else
                    return FindOwner<Chord>().Position - 0.38;
            }
        }

        /// <summary>
        /// Gets or sets the staff position of the note.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="Int32"/>
        /// </para>
        /// <para>
        /// The staff position of the note.
        /// </para>
        /// </value>
        public int StaffPosition
        {
            get { return GetPropertyValue<int>("StaffPosition"); }
            set
            {
                if (UnsavedChanges.ContainsKey("StaffPosition"))
                {
                    var cmd = (SetterCommand<int>)UnsavedChanges["StaffPosition"];
                    cmd.Value = value;
                }
                else
                    UnsavedChanges["StaffPosition"] = CreateSetStaffPositionCommand(value);

                RaisePropertyChanged("StaffPosition");
                RaisePropertyChanged("VerticalPosition");
                RaisePropertyChanged("DotsPosition");
            }
        }

        /// <summary>
        /// Gets or sets the type size of the note.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="Double"/>
        /// </para>
        /// <para>
        /// The type size (in device independent pixels) of the note.<br/>
        /// The value <see cref="Double.NaN"/> makes the note inherit the <see cref="Chord"/>'s size (default).
        /// </para>
        /// </value>
        public double TypeSize
        {
            get
            {
                var modelSize = GetPropertyValue<double>("TypeSize");

                return (double)LengthConverter.Convert(modelSize, typeof(double), null, CultureInfo.CurrentCulture);
            }
            set
            {
                if (UnsavedChanges.ContainsKey("TypeSize"))
                {
                    var cmd = (SetterCommand<double>)UnsavedChanges["TypeSize"];
                    cmd.Value = (double)LengthConverter.ConvertBack(value, typeof(double), null, CultureInfo.CurrentCulture);
                }
                else
                    UnsavedChanges["TypeSize"] = CreateSetTypeSizeCommand(value);
            }
        }

        /// <summary>
        /// Gets the vertical position of the note.
        /// </summary>
        public double VerticalPosition
        {
            get { return FindOwner<Staff>().Bottom - StaffPosition * (FindOwner<Staff>().SpaceSize / 2); }
        }

        /// <summary>
        /// Gets the width of the note.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="Double"/>
        /// </para>
        /// <para>
        /// The width (in device independent pixels) of the note.
        /// </para>
        /// </value>
        public double Width
        {
            get
            {
                return (double)LengthConverter.Convert(((Model.Elements.CWNotation.Note)AdaptedElement).Width, typeof(double), null, CultureInfo.CurrentCulture);
            }
        }

        #endregion

        #region Private Fields

        /// <summary>
        /// Holds a user-friendly name of this element.
        /// </summary>
        private const string elementNameKey = "ViewModel.ElementNames.Note";        
        
        #endregion
    }
}
